/*
Copyright 2021 LuJun

Permission is hereby granted, free of charge, to any person obtaining a copy 
of this software and associated documentation files (the "Software"), to deal 
in the Software without restriction, including without limitation the rights 
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
copies of the Software, and to permit persons to whom the Software is furnished 
to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in 
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/

/// <reference path="../../log/log-manager.ts" />
/// <reference path="../../network/net-line.ts" />

namespace lcc {

const Http = require('http');
const Https = require('https');

const TAG = "lcc/network/http/http-line.ts";

/**
 * @zh
 * HTTP心跳，不大于0表示不使用
 */
export let HTTP_HEARTTICK = -1;

/**
 * @zh
 * HTTP连接关闭时间
 */
export let HTTP_CONNECT_CLOSETIME = 60 * 10;

/**
 * @zh
 * Http网络线配置
 */
export interface HttpConfig {
	/**
     * @zh
	 * URL连接<br/>
     * 客户端
	 */
    url?:string, 

    /**
     * @zh
     * 调用方式， 默认POST<br/>
     * 客户端
     */
    call?:HttpCall;
    
    /**
     * @zh
     * 端口<br/>
     * 服务器
     */
	port?:number
	
	/**
     * @zh
	 * TSL 配置<br/>
     * 服务器
	 */
    tslConfig?:TSLConfig;

    /**
     * @zh
     * 回应<br/>
     * 连接
     */
    response?:any;
}

/**
 * @zh
 * HTTP网络线
 */
export class HttpLine extends NetLine {
    
	/**
     * @zh
	 * 网络线配置
	 */
	private _config:HttpConfig = null;
	
	/**
     * @zh
	 * 网络线实体
	 */
    private _entity:any = null;
    
    /**
     * @zh
     * 连接ID
     */
    private _connectID:string = null;
	
	/**
     * @zh
	 * 就绪
	 */
    private _ready:boolean = false;
    
    /**
     * @zh
     * 连接关闭时间
     */
    private _closeTime:number = 0;

    /**
     * @zh
     * 已经关闭
     */
    private _closed:boolean = false;

    /**
     * @zh
     * 发送心跳时间
     */
    private _sendHeartTime:number = 0;

    /**
     * @zh
     * 延迟发送
     */
    private _delaySend:boolean = false;
	
	/**
     * @zh
	 * 获得URL连接
	 */
	public getURL(){
		return this._config.url;
	}

	/**
     * @zh
	 * 获得端口
	 */
	public getPort(){
		return this._config.port;
    }

    /**
     * @zh
     * 执行延迟发送<br/>
     * **注意，针对HTTP的CONNECTION，如果不能马上写入协议需要调用这个函数，等到写入后再手动调用'flushSendCaches'**
     */
    public delaySend(){
        this._delaySend = true;
    }
    
	/**
     * @zh
	 * 配置网络线
	 * 
	 * @param config - 配置对象
	 */
	public config(config:HttpConfig){
        DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} config`);
		let parent = this.getParent();
		if(parent){
			this.useProtoBuf(parent.isUseProtoBuf());
		}
        if(this._mode == NetLineMode.CONNECT){
            if(this._config && this._config.response){
                this._config.response.json({ type : netdata.NetDataType.PROTOCOLS });
                this._config.response = null;
            }
            this._config = config;
            this._ready = !!this._config.response;
        }else{
            this._config = config;
        }
    }
    
	/**
     * @zh
	 * 检查网络线是否有效
	 */
	public isValid() {
		return this._ready && !this._closed;
	}
    
    /**
     * @zh
     * 处理网络线数据
     * 
     * @param netData 网络数据
     */
    private onDealNetData(netData:NetData){
        DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} onDealNetData ${JSON.stringify(netData)}`);
        if(this._mode === NetLineMode.CONNECT){
            this._closeTime = 0;
            DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} onDealNetData closeTime = 0`);
        }
        if(netData.protocols){
            this.pushDispatchCaches(netData.protocols);
        }
        if(netData.type == netdata.NetDataType.SYNC_CONNECT_ID){
            this._connectID = (netData.data as netdata.SyncConnectID).connectID;
            DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} onDealNetData SYNC_CONNECT_ID ${this._connectID}`);
            if(!this._ready){
                DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} CLIENT connect`);
                this._sendHeartTime = 0;
                this._ready = true;
                try{
                    this.emit("onOpen", true, this);
                }catch(e){
                    logMgr.debug(TAG, `${this.getName()} onDealNetData SYNC_CONNECT_ID Exception : ${e}`);
                }
            }
        }
    }
    
	/**
     * @zh
	 * 打开网络线
	 */
    public async open():Promise<boolean>{
		if(this._mode === NetLineMode.SERVER){
            if(!this._ready){
                DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} SERVER open ${this._config.port} ${this._config.tslConfig}`);
                let lineExpress =  express.createProtocolExpress();
                lineExpress.set('port', this._config.port);
                lineExpress.set('server', this.getName());
                let server = null;
                if(this._config.tslConfig){
                    server = Https.createServer(this._config.tslConfig, lineExpress);
                }else{
                    server = Http.createServer(lineExpress);
                }
                server.on('error', (error)=>{
                    if (error.syscall == 'listen') {
                        let bind = typeof this._config.port === 'string'
                        ? 'Pipe ' + this._config.port
                        : 'Port ' + this._config.port;
                        
                        // handle specific listen errors with friendly messages
                        switch (error.code) {
                            case 'EACCES':
                                logMgr.error(TAG, bind + ' requires elevated privileges');
                                break;
                            case 'EADDRINUSE':
                                logMgr.error(TAG, bind + ' is already in use');
                                break;
                            default:
                        }
                        try{
                            this.emit("onOpen", false, this);
                        }catch(e){
                            logMgr.debug(TAG, `${this.getName()} SERVER open error Exception : ${e}`);
                        }
                    }
                });
                server.on('listening', ()=>{
                    let addr = server.address();
                    let bind = typeof addr === 'string'
                    ? 'pipe ' + addr
                    : 'port ' + addr.port;
                    DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} SERVER listening ${bind}`);

                    this._ready = true;
                    try{
                        this.emit("onOpen", true, this);
                    }catch(e){
                        logMgr.debug(TAG, `${this.getName()} SERVER open listening Exception : ${e}`);
                    }
                });
                this._entity = server;
                server.listen(this._config.port);
            }
        }else if(this._mode === NetLineMode.CLIENT){
            if(!this._ready){
                this._config.call = this._config.call || HttpCall.POST;
                DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} CLIENT open ${this._config.url} ${this._config.call}`);
                if(this._config.url){
                    this._config.url = `${this._config.url}${express.HTTP_PROTOCOL_PATH}`;
                    this.syncConnectID();
                }else{
                    logMgr.error(TAG, `HttpLine ${this._name} url is invalid!`);
                    try{
                        this.emit("onOpen", false, this);
                    }catch(e){
                        logMgr.debug(TAG, `${this.getName()} CLIENT open Exception : ${e}`);
                    }
                }
            }
        }
		return new Promise((resolve)=>{ 
            if(this._ready){
                resolve(true);
            }else{
                this.once(`onOpen`, resolve); 
            }
        });
	}
    
	/**
     * @zh
	 * 关闭网络线
	 */
	public close(){
        if(this._mode === NetLineMode.SERVER){
            if(this._entity){
                DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} SERVER close`);
                this._entity.close();
                this._entity = null;
                this._ready = false;
                try{
                    this.emit("onClose", this, 0);
                }catch(e){
                    logMgr.debug(TAG, `${this.getName()} SERVER close Exception : ${e}`);
                }
                netServer._removeConnectLines(this);
            }
        }else if(this._mode === NetLineMode.CONNECT){ // CONNECT
            if(!this._closed){
                this._closed = true;
                this._ready = false;
                if(this._config && this._config.response){
                    this._config.response.json({ type : netdata.NetDataType.PROTOCOLS });
                    this._config.response = null;
                }
                DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} CONNECT close`);
                try{
                    this.emit("onClose", this, 0);
                }catch(e){
                    logMgr.debug(TAG, `${this.getName()} CONNECT close Exception : ${e}`);
                }
                netServer._removeConnectLine(this._name);
            }
        }else{  // CLIENT
            if(this._ready){
                DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} CLIENT close`);
                this._ready = false;
                try{
                    this.emit("onClose", this, 0);
                }catch(e){
                    logMgr.debug(TAG, `${this.getName()} CLIENT close Exception : ${e}`);
                }
            }
        }
	}
	
	/**
     * @zh
	 * 更新网络线
     * 
	 * @param dt 间隔时间
	 */
	public onUpdate(dt:number){
        if(this._mode === NetLineMode.CONNECT){
            if(HTTP_CONNECT_CLOSETIME > 0){
                this._closeTime += dt;
                if(this._closeTime >= HTTP_CONNECT_CLOSETIME){
                    DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} onUpdate closeTime close`);
                    this.close();
                }
            }
        }else if(this._mode === NetLineMode.CLIENT){
            if(HTTP_HEARTTICK > 0 && this._ready){
                this._sendHeartTime += dt;
                if(this._sendHeartTime >= HTTP_HEARTTICK){
                    DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} onUpdate sendHeartTime`);
                    this.writeNetLineData(netdata.NetDataType.HERT_TICK);
                }
            }
        }
    }
    
    /**
     * @zh
     * 当接收网络数据
     * 
     * @param netData - 网络数据
     * @param config - 配置对象
     */
    public _onRecvNetData(netData:NetData, config:HttpConfig){
        if(this._ready){
            DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} _onRecvNetData ${this._mode} ${JSON.stringify(netData)}`);
            if(this._mode === NetLineMode.SERVER){
                if(netData.connectID){
                    let netline = netServer._getConnectLine(netData.connectID) as HttpLine;
                    if(!netline){
                        DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} _onRecvNetData new HttpLine CONNECT ${netData.connectID}`);
                        netline = new HttpLine(netData.connectID, NetLineMode.CONNECT, NetLineType.HTTP, this);
                        netline.autoSend(false);
                        netServer._addConnectLine(netline);
                        netline._delaySend = false;
                        try{
                            this.emit("newConnect", netline);
                        }catch(e){
                            logMgr.debug(TAG, `${this.getName()} _onRecvNetData SERVER Exception : ${e}`);
                        }
                    }else{
                        netline._delaySend = false;
                    }
                    netline.config(config);
                    netline.onDealNetData(netData);
                    if(!netline._delaySend){
                        netline.flushSendCaches(true);
                    }
                }else{
                    if(netData.type == netdata.NetDataType.SYNC_CONNECT_ID){
                        let newConnectID = `${NetLineType.HTTP}|${netServer._newConnectName()}`;
                        DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} _onRecvNetData SYNC_CONNECT_ID ${newConnectID}`);
                        let data:NetData = {
                            type : netdata.NetDataType.SYNC_CONNECT_ID,
							data : {
								connectID : newConnectID,
							}
                        }
                        config.response.json({ data : packNetData(data, this.isUseProtoBuf()).toString('base64') });
                    }
                }
            }
        }
    }

    /**
     * @zh
     * 同步连接ID
     */
    protected syncConnectID(){
        if(this._mode === NetLineMode.CLIENT){
            DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} syncConnectID`);
            let data:NetData = {
                type : netdata.NetDataType.SYNC_CONNECT_ID
            };
            this.sendNetData_client(data);
        }
    }

    /**
     * @zh
     * 发送网络数据-客户端
     * 
     * @param netData - 网络数据
     */
    private async sendNetData_client(netData:NetData){
		let datab64 = packNetData(netData, this.isUseProtoBuf()).toString('base64');
        let r:any;
        if(this._config.call == HttpCall.POST){
            r = await http.postJson(this._config.url, { data : datab64 });
        }else{ // GET
            r = await http.getJson(this._config.url + "?data=" + datab64);
        }
		let rNetData:NetData = r && unpackNetData(Buffer.from(r.data, 'base64'));
        if(rNetData){
            this.onDealNetData(rNetData);
        }
    }
    
	/**
     * @zh
	 * 发送网络线数据
     * 
	 * @param netData - 网络数据
	 */
	protected sendNetData(netData:NetData){
        if(this._ready){
            DEBUG.NETWORK && logMgr.debug(TAG, `${this.getName()} sendNetData ${this._mode} ${JSON.stringify(netData)}`);
            if(this._mode === NetLineMode.CLIENT){
                netData.connectID = this._connectID;
                this.sendNetData_client(netData);
                this._sendHeartTime = 0;
            }else if(this._mode === NetLineMode.CONNECT){
                this._config.response.json({ data : packNetData(netData, this.isUseProtoBuf()).toString('base64') });
                this._ready = false;
                this._config.response = null;
            }
        }else{
			logMgr.warn(TAG, `HttpLine ${this._name} sendNetData fail!`);
		}
	}
}

//注册网络类型
NetLine.registerNetType(NetLineType.HTTP, HttpLine);

}
